package risoe.syslab.fhnew.hw;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;
import jssc.SerialPortTimeoutException;

import risoe.syslab.fhnew.config.ActuatorConfig;
import risoe.syslab.fhnew.config.FlowmeterInterfaceConfig;
import risoe.syslab.fhnew.config.HWInterfaceConfig;
import risoe.syslab.fhnew.config.SensorConfig;
import risoe.syslab.model.CompositeMeasurement;
import risoe.syslab.model.Quality;
import risoe.syslab.model.Source;
import risoe.syslab.model.Validity;

/**
 * Hardware Interface for communicating with the flowmeter in PowerFlexHouse3 using a serial connection.
 * @author M. Svendsen
 */
public class FlowmeterInterface extends HardwareInterface implements Runnable, ConnectionManagerInterface {
	private static final int BAUD_RATE = 1200;
	private static final int RECEIVE_TIMEOUT = 1000;
	private String mSerialAddress;
	private SerialPort mSerialPort;
	private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
	private ConnectionManager mConnectionManager;
	private int mErrorCount = 0;
	private int mReadCount = 0;

	// Poll every second
	private static final long POLLCYCLE=1000;

	// List containing the variables in use by the system
	private HashMap<String, String> mUsedVariablesMap = new HashMap<String, String>();


	/**
	 * Map of units - TODO: Add missing units
	 */
	//	private static final Map<Integer, String> mUnitMap;
	//	static {
	//		Map<Integer, String> aMap = new HashMap<Integer, String>();
	//		aMap.put(0, "");
	//		aMap.put(1, "Wh");
	//		aMap.put(2, "kWh");
	//		aMap.put(3, "MWh");
	//		aMap.put(5, "j");
	//		aMap.put(6, "kj");
	//		aMap.put(22, "kW");
	//		aMap.put(37, "C");
	//		aMap.put(39, "l");
	//		aMap.put(40, "m3");
	//		aMap.put(44, "ton");
	//		aMap.put(41, "l/h");
	//		aMap.put(42, "m3/h");
	//		aMap.put(43, "m3 x C");
	//		aMap.put(52, "bar");
	//		mUnitMap = Collections.unmodifiableMap(aMap);
	//	}

	/**
	 * Map of variables that can be read
	 */
	private static final Map<String, Integer> mVariableMap;
	static {
		Map<String, Integer> aMap = new HashMap<String, Integer>();
		aMap.put("Heat Energy", 0x003c);
		aMap.put("Control Energy", 0x005e);
		aMap.put("Cooling Energy", 0x003f);
		aMap.put("Flow Energy", 0x003d);
		aMap.put("Return Flow Energy", 0x003e);
		aMap.put("Tap Water Energy", 0x005f);
		aMap.put("Heat Energy Y", 0x0060);
		aMap.put("Energy Reg 8", 0x0061);
		aMap.put("Energy Reg 9", 0x006e);
		aMap.put("Volume Reg V1", 0x0044);
		aMap.put("Volume Reg V2", 0x0045);
		aMap.put("Mass Reg V1", 0x0048);
		aMap.put("Mass Reg V2", 0x0049);
		aMap.put("Flow Temperature", 0x0056);
		aMap.put("Return Flow Temperature", 0x0057);
		aMap.put("Temperature T3", 0x0058);
		aMap.put("Temperature T4", 0x007a);
		aMap.put("Flow Pressure", 0x005b);
		aMap.put("Return Flow Pressure", 0x005c);
		aMap.put("Flow", 0x004a);
		aMap.put("Return Flow", 0x004b);
		aMap.put("Instantaneous Power", 0x0050);
		mVariableMap = Collections.unmodifiableMap(aMap);
	}

	/**
	 * Map of escaped characters
	 */
	private static final int[] mEscapeMap = new int[]{0x06, 0x0d, 0x1b, 0x40, 0x80}; 

	/**
	 * Returns a bytearray representing a message to be sent to the flowmeter.
	 * @param variableId the id of the variable for which a message should be obtained
	 * @return an array of bytes representing the message
	 */
	private int[] getMessage(int variableId){
		ArrayList<Integer> a = new ArrayList<Integer>();

		final int[] bytes = new int[]{0x3f, 0x10, 0x01, variableId >> 8, variableId & 0xff};
		for (int b : bytes){
			a.add(b);
		}

		a.add(0);
		a.add(0);

		// Compute the CRC value
		final int crcBytes = crc_1021(a);

		a.set(a.size()-2, crcBytes >> 8);
		a.set(a.size()-1, crcBytes & 0xff); 


		ArrayList<Integer> c = new ArrayList<Integer>();
		c.add(0x80);

		for (Integer b : a){
			boolean match = false;
			for (int i=0; i<mEscapeMap.length; i++){
				match = false;
				if (b.byteValue() == mEscapeMap[i]){
					// Match
					c.add(0x1b);
					c.add(i ^ 0xff);
					match = true;
					break;
				}
			}
			if (!match){
				c.add(b);
			}
		}
		c.add(0x0d);

		// Convert to an int[] array
		Integer[] intArray = c.toArray(new Integer[0]);
		int[] message = new int[intArray.length];
		for (int i=0; i<intArray.length; i++){
			message[i] = intArray[i].byteValue();
		}

		return message;
	}

	/**
	 * Method for receiving a requested measurement. This method blocks until the entire response has been obtained.
	 * A timeout of 1 second is used when reading. If this timeout is exceeded, the message is scrapped.
	 * @return an array of bytes representing the received message
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ExecutionException
	 * @throws SerialPortException 
	 * @throws TimeoutException is thrown if the timeout period is exceeded
	 */
	private ArrayList<Integer> receive() throws IOException, InterruptedException, ExecutionException, SerialPortTimeoutException, SerialPortException{
		final ArrayList<Integer> b = new ArrayList<Integer>();
		while (true){

			// Start the read with a 1 second timeout
			final int d = mSerialPort.readIntArray(1, RECEIVE_TIMEOUT)[0];
			
			if (d == 0x40){
				// This character marks the beginning of a message
				b.clear();
			}
			b.add(d);

			if (d == 0x0d){
				// This character marks the end of a message
				break;
			}
		}

		final ArrayList<Integer> c = new ArrayList<Integer>();
		int i = 1;
		while (i<b.size()-1){
			if (b.get(i) == 0x1b){
				final int v = (int) (b.get(i+1) ^ 0xff);

				c.add(v);
				i += 2;
			}
			else{
				c.add(b.get(i));
				i += 1;
			}
		}

		c.remove(c.size()-1);
		c.remove(c.size()-1);

		return c;
	}

	public FlowmeterInterface(HWInterfaceConfig hic) {
		super(hic);

		mSerialAddress = ((FlowmeterInterfaceConfig)hic).getSerialAddress();
	}

	@Override
	public void run() {
		// Update and notify process image
		if (mConnectionManager.isConnected()){
			try{
				updateAll();
			}
			catch (Exception e) {
				getLogger().logEvent(getName(), "Error", "Could not poll devices, error durring updating: "+e.getMessage());
			}
		}
		else{
			getLogger().logEvent(getName(), "Error", "Could not poll devices, interface not connected");
		}
	}

	/**
	 * Polls the flowmeter for a single measurement
	 * @param sensorName name of the sensor to poll
	 * @return a CompositeMeasurement or null if an error occurred
	 */
	private CompositeMeasurement poll(String sensorName){
		if (!mConnectionManager.isConnected()){
			getLogger().logEvent(getName(), "Error", "Error polling sensor: "+sensorName+", interface not connected");
			return null;
		}

		int[] message = null;

		String key = mUsedVariablesMap.get(sensorName);
		if (key != null){
			Integer variable = mVariableMap.get(key);
			if (variable != null){
				message = getMessage(variable);
			}
		}

		if (message != null){
			byte[] output = new byte[message.length];
			for (int i=0; i<message.length; i++){
				output[i] = (byte) message[i];
			}

			try {
				mSerialPort.writeBytes(output);
			} catch (SerialPortException e) {
				mConnectionManager.onDisconnected();
				return null;
			}

			// Receive
			mReadCount++;
			ArrayList<Integer> result = null;
			try {
				result = receive();
			}
			catch (IOException e){
				mConnectionManager.onDisconnected();
				return null;
			}
			catch (Exception e){
				mErrorCount++;
				System.out.println("FlowMeter: Timeout during read. Error "+mErrorCount+" out of "+mReadCount+" reads.");
				getLogger().logEvent(getName(), "Error", "Timeout during read. Error "+mErrorCount+" out of "+mReadCount+" reads.");
				return null;
			}

			// Decode the mantissa
			int x = 0;
			// Check size of result so no OOB exceptions can occur
			if (result != null && result.size() > 5 && result.size() >= result.get(5)+7){
				for (int i=0; i<result.get(5); i++){
					x <<= 8;
					x |= result.get(i + 7);
				}
			}


			if (x != 0){
				// Decode the exponent
				int i = (result.get(6) & 0x3f);
				if ((result.get(6) & 0x40) != 0){
					i *= -1;
				}
				double pow = Math.pow(10, i);
				if ((result.get(6) & 0x80) != 0){
					pow *= -1.0;
				}
				double value = ((double)x) * pow;
				return new CompositeMeasurement(value, Quality.OK, Validity.GOOD, Source.PROCESS);
			}
		}

		return null;
	}

	/**
	 * Does a blocking read of all variables in use by the system and notifies the ProcessImage of new readings
	 */
	private void updateAll(){
		// Go through all sensors in use and update them
		for (String sensorName : mUsedVariablesMap.keySet()){
			CompositeMeasurement meas = poll(sensorName);
			if (meas != null){
				mData.put(sensorName, meas);
			}
			else{
				// Mark data as invalid
				CompositeMeasurement measurement = new CompositeMeasurement(Double.NaN, Quality.FAILURE, Validity.INVALID, Source.PROCESS);
				mData.put(sensorName, measurement);
			}
		}

		// Notify the processimage
		notifyProcessImage();
	}

	@Override
	protected void startInterfaceImpl(ArrayList<SensorConfig> sensors, ArrayList<ActuatorConfig> actuators) {
		// Check which variables should be used
		for (SensorConfig sensor : sensors){
			if (sensor.getInterfaceName().equals(getName())){
				// This sensor belongs to this interface
				mUsedVariablesMap.put(sensor.getName(), sensor.getType().mTag);
			}
		}

		getLogger().logEvent(getName(), "Initialization", "Complete");

		// Hand off the connection specifics to the ConnectionManager
		mConnectionManager = new ConnectionManager(this, 0);
		mConnectionManager.startConnection();

		// Start polling
		mExecutor.scheduleAtFixedRate(this, 0, POLLCYCLE, TimeUnit.MILLISECONDS);
	}

	@Override
	public boolean connect(){
		try {
			// Find the correct port and open a connection
			mSerialPort = new SerialPort(mSerialAddress);
			mSerialPort.openPort();

			// Initialize the serial port
			mSerialPort.setParams(BAUD_RATE, SerialPort.DATABITS_8,
					SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);

			// Listen for events
			mSerialPort.addEventListener(new SerialPortEventListener() {

				@Override
				public void serialEvent(SerialPortEvent event) {
					if (event.isERR()){
						mConnectionManager.onDisconnected();
					}
				}
			}, SerialPort.MASK_ERR);

			return true;
		} catch (SerialPortException e){
			e.printStackTrace();
			getLogger().logEvent(getName(), "Connection", "Error connecting: "+e.getMessage());
		}

		return false;
	}

	@Override
	public void disconnect() {
		// End serial eventing and close port
		if (mSerialPort != null){
			try {
				mSerialPort.closePort();
			} catch (SerialPortException e1) {
				e1.printStackTrace();
			}
			mSerialPort = null;
		}
	}

	@Override
	public boolean probeConnection() {
		// No need to check connect since we already poll every second
		return false;
	}

	@Override
	protected void stopInterfaceImpl() {
		mExecutor.shutdownNow();
		mConnectionManager.stopConnection();
	}

	@Override
	public boolean isConnected() {
		return mConnectionManager.isConnected();
	}

	@Override
	public CompositeMeasurement read(Sensor s) {
		return poll(s.getName());
	}

	@Override
	public void applyActuation(Actuator actuator) {
		// The Flowmeter Interface contains no actuators
	}

	/**
	 * Calculates a CRC value used when sending messages to the flowmeter
	 * @param message an array of integers for calculating the CRC value
	 * @return an integer representing the result
	 */
	public static int crc_1021(ArrayList<Integer> message){
		int poly = 0x1021;
		int reg = 0x0000;

		for (int byteInMessage : message){
			int mask = 0x80;
			while(mask > 0){
				reg <<= 1;
				if ((byteInMessage & mask) != 0){
					reg |= 1;
				}
				mask >>= 1;
				if ((reg & 0x10000) != 0){
					reg &= 0xffff;
					reg ^= poly;
				}
			}
		}

		return reg;
	}
}
